﻿using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Runtime.InteropServices;
using System.Security.Cryptography;
using System.Text;
using System.Text.RegularExpressions;
using Threading = System.Threading.Tasks;

namespace LibArchivumAeternum
{
    public class ArchivumAeternum
    {
        #region Constructors

        public ArchivumAeternum(Configuration configFile)
        {
            ConfigFile = configFile;
        }

        #endregion

        #region Public properties

        public List<Task> Tasks
        {
            get
            {
                return ConfigFile.Tasks;
            }
        }

        public Task CurrentTask
        {
            get;
            private set;
        }

        #endregion

        #region Private properties

        private string LatestBackup
        {
            get
            {
                return Directory.EnumerateDirectories(CurrentTask.Destination).OrderBy(p => p).LastOrDefault();
            }
        }

        #endregion

        #region Public events

        public event Action<object, string> OnStepStarted;
        public event Action<object, string> OnStepFinished;
        public event Action<object, string> OnStepFailed;
        public event Action<object> OnBackupFinished;

        #endregion

        #region Public methods

        public void StartBackup(Task task)
        {
            if (BackupTask != null && BackupTask.Status == Threading.TaskStatus.Running)
            {
                return;
            }

            CurrentTask = task;

            if (!Directory.Exists(CurrentTask.Destination))
            {
                Directory.CreateDirectory(CurrentTask.Destination);
            }

            latestBackup = LatestBackup;

            if (OnStepStarted != null)
            {
                OnStepStarted(this, "Latest backup: " + latestBackup);
            }

            FileFilters = new List<Regex>();
            DirectoryFilters = new List<Regex>();

            foreach (Exclude exclude in task.Excludes)
            {
                if (exclude.ApplyTo.HasFlag(ApplyTo.Files))
                {
                    FileFilters.Add(new Regex(exclude.Pattern));
                }

                if (exclude.ApplyTo.HasFlag(ApplyTo.Directories))
                {
                    DirectoryFilters.Add(new Regex(exclude.Pattern));
                }
            }

            string timeStamp = DateTime.Now.ToString("yyyy-MM-dd HH-mm-ss");
            Destination = CurrentTask.Destination + Path.DirectorySeparatorChar + timeStamp;
            Directory.CreateDirectory(Destination);

            BackupTask = Threading.Task.Factory.StartNew(() =>
                {
                    try
                    {
                        foreach (string source in CurrentTask.Sources)
                        {
                            Walk(source);
                        }

                        if (OnBackupFinished != null)
                        {
                            OnBackupFinished(this);
                        }
                    }
                    catch (Exception ex)
                    {
                        if (OnStepFailed != null)
                        {
                            OnStepFailed(this, ex.Message);
                        }
                    }
                });
        }

        public void WaitForBackup(int timeOut = -1)
        {
            try
            {
                BackupTask.Wait(timeOut);
            }
            catch (Exception ex)
            {
                if (OnStepFailed != null)
                {
                    OnStepFailed(this, ex.Message);

                    if (ex.InnerException != null)
                    {
                        OnStepFailed(this, ex.InnerException.Message);
                    }

                    OnStepFailed(this, ex.StackTrace);
                }
            }
        }

        public void StartBatchBackup()
        {
            foreach (Task task in ConfigFile.Tasks)
            {
                StartBackup(task);
                WaitForBackup();
            }
        }

        #endregion

        #region Private members

        private string Destination;
        private Configuration ConfigFile;
        private Threading.Task BackupTask;
        private string latestBackup;
        private List<Regex> FileFilters;
        private List<Regex> DirectoryFilters;

        #endregion

        #region Private methods

        private bool CompareFiles(string sourcePath, string destinationPath)
        {
            try
            {
                if (!File.Exists(sourcePath))
                {
                    return false;
                }

                if (!File.Exists(destinationPath))
                {
                    return false;
                }

                if (CurrentTask.EqualityCheck.HasFlag(EqualityCheck.Size))
                {
                    FileInfo sourceFileInfo = new FileInfo(sourcePath);
                    FileInfo destinationFileInfo = new FileInfo(destinationPath);

                    if (sourceFileInfo.Length != destinationFileInfo.Length)
                    {
                        return false;
                    }
                }

                if (CurrentTask.EqualityCheck.HasFlag(EqualityCheck.ModificationDate))
                {
                    if (File.GetLastWriteTime(sourcePath) != File.GetLastWriteTime(destinationPath))
                    {
                        return false;
                    }
                }

                if (CurrentTask.EqualityCheck.HasFlag(EqualityCheck.CheckSum))
                {
                    if (CalculateSha512Hash(sourcePath) != CalculateSha512Hash(destinationPath))
                    {
                        return false;
                    }
                }

                return true;
            }
            catch
            {
                return false;
            }
        }

        private string CalculateSha512Hash(string path)
        {
            using (FileStream stream = File.OpenRead(path))
            {
                SHA512Cng sha = new SHA512Cng();
                byte[] hash = sha.ComputeHash(stream);
                return BitConverter.ToString(hash);
            }
        }

        private void CopyFile(string sourcePath, string destinationPath)
        {
            File.Copy(sourcePath, destinationPath);
        }

        [DllImport("Kernel32.dll", CharSet = CharSet.Unicode)]
        static extern bool CreateHardLink(string lpNewFileName, string lpExistingFileName, IntPtr lpSecurityAttributes);

        private void LinkFile(string sourcePath, string destinationPath)
        {
            if (!CreateHardLink(destinationPath, sourcePath, IntPtr.Zero))
            {
                throw new ExternalException(String.Format("Could not create a link '{0}' for '{1}'", destinationPath, sourcePath));
            }
        }

        private void Walk(string path)
        {
            foreach (string name in Directory.EnumerateFiles(path))
            {
                if (OnStepStarted != null)
                {
                    OnStepStarted(this, name);
                }

                if (Filter(name, FileFilters))
                {
                    if (OnStepFinished != null)
                    {
                        OnStepFinished(this, "skipped");
                    }

                    continue;
                }

                string newFile = Destination + Path.DirectorySeparatorChar + name.Replace(':', '$');
                string latestFile = latestBackup + Path.DirectorySeparatorChar + name.Replace(':', '$');

                if (String.IsNullOrEmpty(latestBackup) || !CompareFiles(name, latestFile))
                {
                    try
                    {
                        string directoryName = Path.GetDirectoryName(newFile);

                        if (!Directory.Exists(directoryName))
                        {
                            Directory.CreateDirectory(directoryName);
                        }

                        CopyFile(name, newFile);

                        if (OnStepFinished != null)
                        {
                            OnStepFinished(this, "copied");
                        }
                    }
                    catch (Exception ex)
                    {
                        if (OnStepFailed != null)
                        {
                            OnStepFailed(this, ex.Message);
                        }
                    }
                }
                else
                {
                    try
                    {
                        LinkFile(latestFile, newFile);

                        if (OnStepFinished != null)
                        {
                            OnStepFinished(this, "linked");
                        }
                    }
                    catch (Exception ex)
                    {
                        if (OnStepFailed != null)
                        {
                            OnStepFailed(this, ex.Message);
                        }
                    }
                }
            }

            foreach (string name in Directory.EnumerateDirectories(path))
            {
                if (OnStepStarted != null)
                {
                    OnStepStarted(this, name + "...");
                }

                if (Filter(name, DirectoryFilters))
                {
                    if (OnStepFinished != null)
                    {
                        OnStepFinished(this, "skipped");
                    }

                    continue;
                }

                string newDirectory = Destination + Path.DirectorySeparatorChar + name.Replace(':', '$');
                Directory.CreateDirectory(newDirectory);

                Directory.SetCreationTime(newDirectory, Directory.GetCreationTime(name));
                Directory.SetLastWriteTime(newDirectory, Directory.GetLastWriteTime(name));
                Directory.SetLastAccessTime(newDirectory, Directory.GetLastAccessTime(name));

                Walk(name);
            }
        }

        // Returns true if matches
        private bool Filter(string name, List<Regex> filters)
        {
            foreach (Regex filter in filters)
            {
                if (filter.IsMatch(name))
                {
                    return true;
                }
            }

            return false;
        }

        #endregion
    }
}
